package fi;

import shaders.ShaderLoader;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.Mesh;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.VertexAttribute;
import com.badlogic.gdx.graphics.VertexAttributes.Usage;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.math.Frustum;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Plane;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Array;

public class LightHandler {

	static final int BATCH_SIZE = 60;

	static final float TRESHOLD = 1.3f;

	public final Array<PointLight> lights = new Array<PointLight>(true, 16,
			PointLight.class);
	Mesh sphere;
	GFrameBuffer lightBuffer;
	private ShaderProgram pointShader;
	int indices;

	float[] pos = new float[BATCH_SIZE * 4];
	float[] col = new float[BATCH_SIZE * 4];

	LightHandler(GFrameBuffer lightBuffer, Mesh mesh) {
		this.lightBuffer = lightBuffer;
		pointShader = ShaderLoader.createShader("instancedpoint",
				"instancedpoint");

		// add some lights to scene
		lights.add(new PointLight(0, 0, -0.4f, 1.4f, Color.RED));
		lights.add(new PointLight(0, 0, -0.4f, 1.4f, Color.BLUE));
		lights.add(new PointLight(-4f, 1.2f, 0, 4, Color.ORANGE));
		lights.add(new PointLight(-3f, 0.1f, -5, 3, Color.CYAN));
		lights.add(new PointLight(0f, 0f, -2, 4, Color.GREEN));
		lights.add(new PointLight(2f, 0.1f, 0.2f, 3, Color.YELLOW));
		lights.add(new PointLight(0f, 2f, -5, 4, Color.WHITE));

		for (int i = -500; i < 500; i++) {
			lights.add(new PointLight(i / 200f, -1.45f, -3 + i / 150f, 0.35f,
					Color.RED));
		}

		indices = mesh.getNumVertices();

		float[] data = new float[indices * 3];
		mesh.getVertices(data);

		float[] tmp = new float[indices * BATCH_SIZE * 4];

		// for ( int i = indices; --i > 0;){
		//
		// }

		int idx = 0;
		for (int j = 0; j < BATCH_SIZE; j++) {
			int indeksi = 0;
			for (int i = 0; i < indices; i++) {
				tmp[idx++] = data[indeksi++];
				tmp[idx++] = data[indeksi++];
				tmp[idx++] = data[indeksi++];
				tmp[idx++] = j;
			}
		}
		sphere = new Mesh(true, indices * BATCH_SIZE, 0, new VertexAttribute(
				Usage.Position, 4, "a_position"));
		sphere.setVertices(tmp);

		sphere.setAutoBind(false);
	}

	float timer = 0f;

	static float[] viewPos = { 0, 0, 0 };

	public void render(PerspectiveCamera cam) {

		// move lights a bit
		timer += Gdx.graphics.getDeltaTime();
		lights.items[0].x = 0.7f * MathUtils.sinDeg(timer * 100f);
		lights.items[0].y = 0.3f * MathUtils.cosDeg(timer * 100f);
		lights.items[1].x = -0.7f * MathUtils.sinDeg(timer * 81f + 180);
		lights.items[1].y = 0.3f * MathUtils.cosDeg(timer * 81f + 180);
		lights.items[0].z = 0.15f;
		lights.items[1].z = 0.15f;
		for (int i = 2; i < lights.size; i++) {
			lights.items[i].x += 0.005 * MathUtils.sinDeg(timer * 40f + i
					* 212f);
			lights.items[i].z += 0.005 * MathUtils.cosDeg(timer * 40f + i
					* 122f);
		}

		// accumulate lights
		lightBuffer.begin();
		Gdx.gl.glClearColor(0, 0, 0, 0);
		Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

		Gdx.gl.glDepthMask(false);
		Gdx.gl.glEnable(GL10.GL_BLEND);
		Gdx.gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE);

		pointShader.begin();

		pointShader.setUniformf("far", cam.far);
		pointShader.setUniformMatrix("u_projectionMatrix",
				cam.projection, false);

		sphere.bind(pointShader);

		culling(cam.frustum);
		int j = 0;
		// if camera is outside of light sphere
		Gdx.gl.glDepthFunc(GL10.GL_LEQUAL);
		Gdx.gl.glCullFace(GL10.GL_BACK);
		for (int i = 0; i < lights.size; i++) {
			PointLight l = lights.items[i];
			if (!l.culled) {

				final float dis = l.distance
						* TRESHOLD + 0.5f;

				final float a = l.x - cam.position.x;
				final float b = l.y - cam.position.y;
				final float c = l.z - cam.position.z;
				l.outside = a * a + b * b + c * c < dis * dis;
				if (l.outside)
					continue;

				viewPos[0] = l.x;
				viewPos[1] = l.y;
				viewPos[2] = l.z;
				Matrix4.mulVec(cam.view.val, viewPos);
				batch(l);
				j++;
			}
		}
		flush();

		if (j < lights.size) {
			// if camera is inside of light sphere
			Gdx.gl.glDepthFunc(GL10.GL_GEQUAL);
			Gdx.gl.glCullFace(GL10.GL_FRONT);

			for (int i = 0; i < lights.size; i++) {
				PointLight l = lights.items[i];
				if (!l.culled && l.outside) {
					viewPos[0] = l.x;
					viewPos[1] = l.y;
					viewPos[2] = l.z;
					Matrix4.mulVec(cam.view.val, viewPos);
					batch(l);

				}
			}
			flush();
		}
		Gdx.gl.glCullFace(GL10.GL_BACK);

		sphere.unbind(pointShader);
		pointShader.end();

		Gdx.gl.glDisable(GL10.GL_BLEND);
		lightBuffer.end();
	}

	private void culling(Frustum frustum) {

		for (int i = 0; i < lights.size; i++) {
			PointLight l = lights.items[i];
			l.culled = false;
			for (int j = 0; j < frustum.planes.length; j++) {
				final Plane p = frustum.planes[j];
				final Vector3 plane = p.normal;
				if ((plane.x * l.x + plane.y * l.y + plane.z * l.z) < (-l.distance - p.d)) {
					l.culled = true;
					break;
				}
			}
		}
	}

	public void dispose() {
		lightBuffer.dispose();
	}

	int batchCount = 0;

	void batch(PointLight l) {

		batchCount++;

		if (batchCount >= BATCH_SIZE) {
			flush();
		}
		final int i = batchCount * 4;
		pos[i + 0] = viewPos[0];
		pos[i + 1] = viewPos[1];
		pos[i + 2] = viewPos[2];
		pos[i + 3] = 1f / l.distance;

		col[i + 0] = l.r;
		col[i + 1] = l.g;
		col[i + 2] = l.b;
	}

	void flush() {
		if (batchCount == 0)
			return;
		pointShader.setUniform4fv("lightPos", pos, 0, batchCount * 4);
		pointShader.setUniform4fv("lightColor", col, 0, batchCount * 4);
		sphere.render(pointShader, GL10.GL_TRIANGLES, 0, indices * batchCount);

		batchCount = 0;
	}

}
